单一定义规则

  在一个程序里,任一个类、枚举和模板等必须只定义惟一的一次。

  从实践的观点看,这意味着对(比如说)一个类,必须在某个单独的文件里存在着恰好一个类定义。不幸的是,语言的规则不能这么简单。例如,某个类的定义可能是通过宏展开(呜呼!)组合而成的;某个类的定义也可能通过#include指令,以文本方式被包含到两个源文件里(9.2.1节)。更糟糕的是,“文件”并不是C或C++语言定义的一部分;存在着一些实现,它们根本不将程序存储为源文件。

  因此,标准中关于一个类、一个模板等只能有惟一定义的规则,就需要以一种更复杂精细的方式来陈述。这个规则经常被称为“单一定义规则”(One-Definition Rule,ODR)。也就是说:一个类、模板或者在线函数的两个定义能够被接受为同一个惟一定义的实例,当且仅当:

1)、它们出现在不同编译单位里。
2)、它们按一个个单词对应相同。
3)、这些单词的意义在两个编译单位中也完全一样。

例如,

    // file1.c:
        struct S { int a; char b; };
        void f(S*);

    // file2.c:
        struct S { int a; char b; };
        void f(S* p) { /* ... */ }

ODR认为这个例子是合法的,而且S在两个源文件里引用了同一个类。然而,像这样两次写出一个定义很不聪明。维护file2.c的人们将会很自然地假定file2.c里的S的定义就是S的惟一定义,因此认为可以自由地去修改它。这样就会引进难以检查的错误。

ODR的意图是想允许将一个公共源文件里的类定义包含到不同的编译单位里去。例如,

    // file s.h:
        struct S { int a; char b; };
        void f(S*);

    // file1.c:
        #include "s.h"
        // 在这里使用f()

    // file2.c
        #include "s.h"
        void f(S* p) { /* ... */ }

或图示为

下面给出违反ODR的三种情况的例子:

    // file1.c
        struct S1 { int a; char b; };
        struct S1 { int a; char b; };        // 错误❌:重复定义

这是错误的,因为在一个编译单位里,同一个struct不能定义两次。

    // file1.c:
        struct S2 { int a; char b; };

    // file2.c:
        struct S2 { int a; char bb; };        // 错误❌

这是错误的,因为S2被用做两个成员不相同的类的名字。

    // file1.c:
        typedef int X;
        struct S3 { X a; char b; };

    // file2.c:
        typedef char X;
        struct S3 { X a; char b; };            // 错误❌

这里的两个S3定义按单词对应相同。但这个例子是错误的,因为在两个文件里,名字X的意义被偷偷摸摸地做成不同的了。

  在互相分离的编译单位里检查和抵御不一致的类定义,这件事超出了许多实现的能力范围。因此,违背ODR的声明就可能成为难以琢磨的错误的根源。不幸的是,将共享定义放在头文件并#include它们的技术并不能防范上面的最后一种违背ODR的形式。局部typedef和宏都可能改变通过#include包含进来的声明的意义:

    // file s.h:
        struct S { Point a; char b; };

    // file1.c:
        #define Point int
        #include "s.h"
        // ...

    // file2.c:
        class Point { /* ... */ };
        #include "s.h"
        // ...

抵御这类黑客手段的最好办法就是将头文件尽可能做得自给自足。例如,如果类Point已经在 s.h 头文件里声明了,上面这个错误就会被检查出来。

  也允许将一个模板定义#include到多个编译单位里,条件是仍然维持ODR。另外,如果只存在一个声明,就可以使用一个导出的(export)模板

    // file1.c:
        export template<class T> T twice(T t) { return t + t; }

    // file2.c:
        template<class T>T twice(T t);            // 声明
        int g(int i) { return twice(i); }

关键字export的意思就是“在其他的编译单位可以访问”。

🔚